﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

namespace gcr4csharp
{
    public class GCR
    {
        #region Module

#if !UNITY_EDITOR && UNITY_IPHONE
        private const string module = "__Internal";
#else
        private const string module = "gcr.dll";
#endif

        public class MonoPInvokeCallbackAttribute : Attribute
        {
            public MonoPInvokeCallbackAttribute(Type type) { }
        }

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_init(string host);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern void gcr_uninit();

        public delegate void LogCallback(int level, string text);
        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern void set_log_cb(LogCallback log_cb);


        private delegate void gcr_on_new_event(IntPtr ev);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_new_event(string user, string text, gcr_on_new_event on_new_event);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_add_file(IntPtr ev, string path);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_add_file_content(IntPtr ev, string name, byte[] content, int size);


        private delegate int gcr_progress(int off, int total);
        private delegate void gcr_on_commit(IntPtr ev, int code);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_commit(IntPtr ev, gcr_progress progress, gcr_on_commit on_commit);


        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern int gcr_retry(IntPtr ev, gcr_on_commit on_retry);

        [DllImport(module, CallingConvention = CallingConvention.Cdecl)]
        private static extern void gcr_free_event(IntPtr ev);

        #endregion


        #region Log
        public enum LogLevel
        {
            none = 0,
            debug,
            info,
            warn,
            err,
        }
        public struct LogLine
        {
            public LogLevel level;
            public string text;
        }

        public static readonly string[] LogLevelText = { "None", "Debug", "Info", "Warn", "Err" };
        public delegate void Logger(LogLevel level, string text);


        private static Logger logger = null;

        private static ArrayList logs = new ArrayList();
        private const int maxLogCacheLines = 10240;

        [MonoPInvokeCallback(typeof(LogCallback))]
        private static void OnGCRLog(int level, string text)
        {
            if (logs != null)
            {
                lock (logs)
                {
                    if (logs.Count < maxLogCacheLines)
                    {
                        LogLine line = new LogLine
                        {
                            level = (LogLevel)level,
                            text = text
                        };
                        logs.Add(line);
                        return;
                    }
                }
            }
            Console.WriteLine("[{0}] {1}", LogLevelText[level], text);
        }
        #endregion


        #region Global

        private delegate void Callback();
        private static Callback callback = null;

        public static bool Init(string host, Logger logger = null)
        {
            if (logger != null)
            {
                GCR.logger = logger;
                logs = new ArrayList();
                set_log_cb(OnGCRLog);
            }

            return gcr_init(host) == 0;
        }

        public static void UnInit()
        {
            gcr_uninit();
        }

        public static void Update()
        {
            lock (logs)
            {
                foreach (LogLine line in logs)
                    logger(line.level, line.text);
                logs.Clear();
            }

            if (uploading)
            {
                if (progressUpdated && onCommitProgress != null)
                {
                    onCommitProgress(off, total);
                    progressUpdated = false;
                }
            }

            if (callback != null)
            {
                callback();
                callback = null;
            }
        }
#endregion


        public delegate void OnCommitResult(bool ok, int code);
        public delegate void OnCommitProgress(int off, int total);

        private static OnCommitResult onCommitResult = null;
        private static OnCommitProgress onCommitProgress = null;

        private struct FileContent
        {
            public string name;
            public byte[] content;
        }

        public static string User; // user json text
        public static string Text;
        private static ArrayList files = new ArrayList();

        private static volatile int off;
        private static volatile int total;
        private static volatile int cancelFlag;

        private static object mutex = new object();
        private static volatile bool uploading = false;
        private static volatile bool progressUpdated = false;

        private static volatile IntPtr retry = IntPtr.Zero;

        public static void AddFile(string path)
        {
            files.Add(path);
        }

        public static void AddFileContent(string name, byte[] content)
        {
            FileContent ct = new FileContent();
            ct.name = name;
            ct.content = content;
            files.Add(ct);
        }

        public static void Cancel()
        {
            cancelFlag = 1;
        }

        public static int Off
        {
            get => off;
        }

        public static int Total
        {
            get => total;
        }

        public static bool Uploading
        {
            get => uploading;
        }

        [MonoPInvokeCallback(typeof(gcr_progress))]
        private static int OnGCRProgress(int off, int total)
        {
            GCR.off = off;
            GCR.total = total;
            GCR.progressUpdated = true;
            return cancelFlag;
        }

        [MonoPInvokeCallback(typeof(gcr_on_commit))]
        private static void OnGCRCommit(IntPtr ev, int code)
        {
            uploading = false;
            callback += () => onCommitResult(code == 0, code);

            if (code == 0 || cancelFlag != 0)
                gcr_free_event(ev);
            else
                retry = ev;
        }

        [MonoPInvokeCallback(typeof(gcr_on_new_event))]
        private static void OnGCRNewEvent(IntPtr ev)
        {
            int ret = 0;
            foreach (var file in files)
            {
                if (file is FileContent)
                {
                    FileContent ct = (FileContent)file;
                    ret = gcr_add_file_content(ev, ct.name, ct.content, ct.content.Length);
                }
                else
                {
                    ret = gcr_add_file(ev, (string)file);
                }

                if (ret != 0)
                    goto Stop;
            }

            ret = gcr_commit(ev, OnGCRProgress, OnGCRCommit);
            if (ret == 0)
                return;

            Stop:
            gcr_free_event(ev);
            uploading = false;
            callback += () => onCommitResult(false, ret);
        }

        public static bool Commit(OnCommitResult result, OnCommitProgress progress = null)
        {
            lock (mutex)
            {
                if (result == null)
                    return false;

                if (uploading)
                    return false;

                if (retry != IntPtr.Zero)
                {
                    gcr_free_event(retry);
                    retry = IntPtr.Zero;
                }

                onCommitProgress = progress;
                onCommitResult = result;

                cancelFlag = 0;
                int ret = gcr_new_event(User, Text, OnGCRNewEvent);
                if (ret != 0)
                    return false;

                uploading = true;
                return true;
            }
        }

        [MonoPInvokeCallback(typeof(gcr_on_commit))]
        private static void OnGCRRetry(IntPtr ev, int code)
        {
            uploading = false;
            callback += () => onCommitResult(code == 0, code);

            if (code == 0)
            {
                gcr_free_event(ev);
                retry = IntPtr.Zero;
            }
            uploading = false;
        }

        public static bool CanRetry
        {
            get => retry != IntPtr.Zero;
        }

        public static bool Retry()
        {
            lock (mutex)
            {
                if (uploading)
                    return false;

                if (retry == IntPtr.Zero)
                    return false;

                cancelFlag = 0;
                int ret = gcr_retry(retry, OnGCRRetry);
                if (ret != 0)
                {
                    gcr_free_event(retry);
                    retry = IntPtr.Zero;
                    return false;
                }

                uploading = true;
                return true;
            }
        }

        public static void Clear()
        {
            Cancel();

            User = null;
            Text = null;

            files.Clear();

            if (!uploading && retry != IntPtr.Zero)
            {
                gcr_free_event(retry);
                retry = IntPtr.Zero;
            }

            if (logs != null)
            {
                logs.Clear();
            }
            callback = null;
        }

    }

}
